package icasue.runninginvoke.invoke;

import icasue.base.touch.ApplicationTouch;
import icasue.base.utils.check.CommonCheck;
import icasue.base.utils.wareable.FieldConvertUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import icasue.tools.compile.utils.DCUtil;
import icasue.reflect.handles.util.OFUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import icasue.reflect.exceptions.HandleException;
import icasue.reflect.handles.arrays.AryOF;
import icasue.reflect.handles.classes.ClassOF;
import icasue.reflect.handles.fields.FieldOF;
import icasue.reflect.handles.method.MethodOF;
import icasue.reflect.handles.object.ObjectOF;
import icasue.reflect.handles.predicate.SureOF;

import javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/9/10 上午10:54
 * @UpdateDate:
 * @Description:
 */
@Slf4j
public class RunningInvoking {

    public static volatile Boolean enable = Boolean.TRUE;

    private static final Map<Class, Class> standardDataTypeClassMapping = new HashMap<Class, Class>(){{
        put(byte.class,     Byte.class);
        put(short.class,    Short.class);
        put(int.class,      Integer.class);
        put(long.class,     Long.class);
        put(float.class,    Float.class);
        put(double.class,   Double.class);
        put(boolean.class,  Boolean.class);
    }};

    public static final String noEnableInfos = "\n\t ######----->> : RunningInvoke Plugins has not enable, " +
            "if you want use this plugin, add @DiscoveryExposePoints on your Application's entrance than register one instance of 'icasue.plugin.base.touch.ApplicationTouch' pls.";

    public static volatile ExecutorService executorService = null;

    public static final Supplier<Map<String,Object>> errorInfoSupplier = () -> {
        return new HashMap<String, Object>(2){{
            put("error_code","40x");
        }};
    };

    public static final Supplier<Map<String,Object>> successInfoSupplier = () -> {
        return new HashMap<String, Object>(2){{
            put("success_code","10000");
        }};
    };

    public static void initThreadPool(){
        executorService = new ThreadPoolExecutor(
                5, Integer.MAX_VALUE, 0, NANOSECONDS,new LinkedBlockingDeque<>()
        );
    }


    /**
     * 公共方法调度，调度成功返回结果.
     * This method support a strong function,allow developer invoke application's method on running.
     * return target method's execute result.
     * notice: 1.if targetClass is exist as a inner class,we need support clearly full className.
     *         2.targetClass's scope is component which exist in ApplicationContext or BeanFactory or prototype.
     *         3.targetMethod's invoke params not support enum, interface.
     *         4.request method invoke params can be lacked.
     * @param invokerBucket
     * @return
     */
    public static Object commonInvoke(InvokerBucket invokerBucket,Boolean waitingResult) {
        if(enable != null && false == enable.booleanValue()){
            log.info(noEnableInfos);
            return RunningInvoking.getErrorInfo(noEnableInfos);
        }
        //waiting environment init.
        Object waitingEnvironmentInitError = waitingEnvironmentReady();
        Callable<Object> callAbleTask = () -> {
            if(waitingEnvironmentInitError != null){
                return waitingEnvironmentInitError;
            }
            //check className and method name.
            if(invokerBucket == null){
                return RunningInvoking.getErrorInfo("方法调度异常 : InvokerBucket不存在.");
            }
            Class<?> targetClass = null;
            String methodName = invokerBucket.getMethodName();
            ArrayList<InvokerBucket.InvokerParams> params = invokerBucket.getParams();
            if(CommonCheck.nullStrings(invokerBucket.getClassName(),methodName)){
                return RunningInvoking.getErrorInfo("方法调度异常 : 无效类名或方法名.");
            }
            try { targetClass = DCUtil.loadClass(invokerBucket.getClassName()); }catch (Throwable e){
                try { targetClass = Class.forName(invokerBucket.getClassName()); }catch (Throwable e2){
                    return RunningInvoking.getErrorInfo("方法调度异常 : 目标调度类不存在.");
                }
            }

            //invoke paramType array and origin valueArray.
            Class<?>[] targetMethodParamsClasses = null;
            Object[] targetMethodParamValues = null;

            //paramType and paramValue array checking and getting.
            if(!CommonCheck.nullList(params)){
                List<? extends Object> paramClass = params.stream()
                        .filter((param) -> !CommonCheck.nullStrings(param.getParamClassName()))
                        .map((param) -> {
                            try {
                                return Class.forName(param.getParamClassName());
                            } catch (ClassNotFoundException e) {
                                return RunningInvoking.getErrorInfo("方法调度异常 : 存在无效方法参数类型.");
                            }
                        }).collect(Collectors.toList());
                //predicate process getting param types no exception.
                boolean noException = paramClass.stream().allMatch(item -> item instanceof Class);
                if(noException && paramClass.size() != params.size()){
                    return RunningInvoking.getErrorInfo("方法调度异常 : 存在空的方法参数类型.");
                }else {
                    targetMethodParamsClasses = new Class[paramClass.size()];
                    targetMethodParamValues = new Object[paramClass.size()];
                    paramClass.toArray(targetMethodParamsClasses);
                }
                //get paramValue array which target method invoke needed.
                List<Object> paramValues = params.stream()
                        .map((param) -> {
                            if(param.getParamValue() != null){
                                try {
                                    //Standard javaType convert.
                                    Class<?> currentParamType = Class.forName(param.getParamClassName());
                                    if(!currentParamType.isAssignableFrom(param.getParamValue().getClass())){
                                        Object baseConvert = FieldConvertUtil.TransSupportJavaType.tryConstructSupportTransObject(currentParamType, param.getParamValue());
                                        if(baseConvert != null){
                                            return baseConvert;
                                        }
                                    }
                                    //when paramType is special type such as a body, mvc parse as LinkedHashMap by default,
                                    //we needs parse map value to target method's virtual JavaType as param.
                                    RunningInvoking.tryInstanceAndReSetBodyParam(param);
                                } catch (Throwable e) {
                                    return RunningInvoking.getErrorInfo("Body参数转换异常 : 请检查是否为内部类,枚举,或构造器和属性权限.");
                                }
                            }
                            return param.getParamValue();
                        })
                        .collect(Collectors.toList());
                paramValues.toArray(targetMethodParamValues);
            }
            Object invokeResult;
            try {
                Object[] methodAndInvoker = MethodOF.findMethodAndInvoker_type_mName_paramClassArray.apply(targetClass, methodName, targetMethodParamsClasses);
                invokeResult = MethodOF.invoke_method_invoker_paramArray_.apply(methodAndInvoker[0],methodAndInvoker[1],targetMethodParamValues);
            } catch (HandleException deepError){
                return RunningInvoking.getErrorInfo(deepError.message);
            }catch (Throwable e) {
                return RunningInvoking.getErrorInfo("方法调度异常 : 参数校验通过，调度方法Process异常.");
            }
            return RunningInvoking.getSuccessInfo(invokeResult);
        };
        Future<Object> futureTask = executorService.submit(callAbleTask);
        if(waitingResult != null && !waitingResult){
            return RunningInvoking.getSuccessInfo("Async Processing...");
        }
        while (!futureTask.isDone()){ }
        try {
            return futureTask.get();
        } catch (Throwable e) {
            return RunningInvoking.getErrorInfo("线程任务结果获取异常 : " + e.getCause());
        }
    }

    public static void tryInstanceAndReSetBodyParam(InvokerBucket.InvokerParams params) throws Throwable {
        String paramClassName = params.getParamClassName();
        Class<?> paramClass = Class.forName(paramClassName);
        Object paramValue = params.getParamValue();

        if(!ClassOF.isAssignableFrom_.test(paramClass,paramValue.getClass())){
            //Target is JSONArray.
            if(ClassOF.isAssignableFrom_.test(JSONArray.class,paramClass)){
                if(ClassOF.isAssignableFrom_.test(List.class,paramValue.getClass())){
                    paramValue = JSON.parseArray(JSON.toJSONString(paramValue));
                }
                SureOF.equals_.accept(Boolean.TRUE,ClassOF.isInstance_.test(JSONArray.class,paramValue));
            }
            //Target is List.
            //notice : List's deep item can't construct by recursion, suggest method's params using Array to replace.
            else if(ClassOF.isAssignableFrom_.test(List.class,paramClass)){
                if(ClassOF.isAssignableFrom_.test(JSONArray.class,paramValue.getClass())){
                    JSONArray cast = OFUtil.cast(paramValue, JSONArray.class);
                    List<Object> listVal = new ArrayList<>(cast.size());
                    cast.forEach(item -> listVal.add(item));
                    paramValue = cast;
                }
                SureOF.equals_.accept(Boolean.TRUE,ClassOF.isInstance_.test(List.class,paramValue));
            }
            //Target is Array.
            else if(ClassOF.isArray_.test(paramClass)){
                if(ClassOF.isAssignableFrom_.test(List.class,paramValue.getClass())){
                    List cast = OFUtil.cast(paramValue, List.class);
                    Class innerItemDataType = null;
                    //java's base data type.
                    if(paramClassName.length() == 2){
                        String baseTypeCode = paramClassName.substring(1);
                        switch (baseTypeCode){
                            case "S" : innerItemDataType = short.class; break;
                            case "B" : innerItemDataType = byte.class; break;
                            case "C" : innerItemDataType = char.class; break;
                            case "I" : innerItemDataType = int.class; break;
                            case "J" : innerItemDataType = long.class; break;
                            case "F" : innerItemDataType = float.class; break;
                            case "D" : innerItemDataType = double.class; break;
                            case "Z" : innerItemDataType = boolean.class; break;
                            default : innerItemDataType = Object.class; break;
                        }
                    }
                    //java's reference data type.
                    else {
                        String refTypeCls = paramClassName.substring(2).substring(0, paramClassName.length() - 3);
                        innerItemDataType = Class.forName(refTypeCls);
                    }
                    Object aryInst = AryOF.create_.apply(innerItemDataType, cast.size());
                    for (int i = 0; i < cast.size(); i++) {
                        //Bay be inner element exist as a reference type or special type, try recursion to construct this param.
                        innerItemDataType = standardDataTypeClassMapping.getOrDefault(innerItemDataType,innerItemDataType);
                        Object digestConvertItem = recursionTransParam(innerItemDataType, cast.get(i));
                        AryOF.set_.apply(aryInst, i, digestConvertItem);
                    }
                    paramValue = aryInst;
                }
                SureOF.equals_.accept(Boolean.TRUE,ClassOF.isArray_.test(paramValue.getClass()));
            }
            //Target is JSONObject.
            else if(ClassOF.isAssignableFrom_.test(JSONObject.class,paramClass)){
                if(ClassOF.isAssignableFrom_.test(Map.class,paramValue.getClass())){
                    paramValue = JSON.parseObject(JSON.toJSONString(paramValue));
                }
                SureOF.equals_.accept(Boolean.TRUE,ClassOF.isInstance_.test(JSONObject.class,paramValue));
            }
            //Target is Map.
            else if(ClassOF.isAssignableFrom_.test(Map.class,paramClass)){
                if(ClassOF.isAssignableFrom_.test(JSONObject.class,paramValue.getClass())){
                    paramValue = JSON.parseObject(paramValue.toString(),Map.class);
                }
                SureOF.equals_.accept(Boolean.TRUE,ClassOF.isInstance_.test(Map.class,paramValue));
            }
            //Target is Special Model.
            else if(ClassOF.isInstance_.test(Map.class,paramValue)){
                Object realInst = paramClass.newInstance();
                Map<String,Object> bodyValues = OFUtil.cast(paramValue, Map.class);
                for (Map.Entry<String, Object> entry : bodyValues.entrySet()) {
                    String fieldName = entry.getKey();
                    Object value = entry.getValue();
                    Field field = FieldOF.findField_ofTp_fName_.apply(paramClass, fieldName);
                    if(value != null && !field.getType().isAssignableFrom(value.getClass())){
                        //Recursion current field, for List / Array
                        // notice : List will be changeTo JSONArray or List<Map> or List<JSONObject>
                        // Array will deep recursion, Array is more prefect, but List is power less,
                        // Suggest to using Array replace List strongly!
                        if(ClassOF.isArray_.test(value.getClass()) || ClassOF.isAssignableFrom_.test(List.class,value.getClass())){
                            value = recursionTransParam(field.getType(), value);
                        }else {
                            value = FieldConvertUtil.TransSupportJavaType.tryConstructSupportTransObject(field.getType(), value);
                        }
                    }
                    FieldOF.set_.accept(field,realInst,value);
                }
                paramValue = realInst;
                SureOF.equals_.accept(Boolean.TRUE,ClassOF.isInstance_.test(paramClass,paramValue));
            }else {
                /** Others wise will ignore param dealing. for developer to discovery. **/
            }
            params.setParamValue(paramValue);
        }
    }


    /**
     * Array or List item's recursion convert.
     * @param targetCls
     * @param currentVal
     * @return
     * @throws Throwable
     */
    private static Object recursionTransParam(Class targetCls,Object currentVal) throws Throwable {
        if(ObjectOF.isNull_.test(currentVal)) return currentVal;
        else if(ClassOF.isInstance_.test(targetCls,currentVal)) return currentVal;
        else {
            InvokerBucket.InvokerParams recursionCurrentParamItem = new InvokerBucket.InvokerParams();
            recursionCurrentParamItem.setParamValue(currentVal);
            recursionCurrentParamItem.setParamClassName(targetCls.getName());
            tryInstanceAndReSetBodyParam(recursionCurrentParamItem);
            return recursionCurrentParamItem.getParamValue();
        }
    };

    /**
     * Appilication environment ready.
     * @return
     */
    private static Object waitingEnvironmentReady(){
        int preInvokeBlockSeconds = 0;
        int preCountDelaySecond = 5;
        int delaySecond = 0;
        while (!ApplicationTouch.canInvoke){
            try {
                TimeUnit.MILLISECONDS.sleep(20);
                preInvokeBlockSeconds += 20;
                if(preInvokeBlockSeconds >= 5000){
                    delaySecond = preInvokeBlockSeconds / 1000;
                    //un jungle if user disable this plugin or Main thread occur block.
                    //print log every second, max waiting time is 30 second. over will throw RuntimeException.
                    if(delaySecond - preCountDelaySecond > 0) {
                        log.info("\n\t ######----->> : {} ",
                                "Sub Thread get PreInvokeEnvironment has Block Over 5 Seconds. Current Block Second : " +
                                        delaySecond + ",Checking if RunningInvoke Plugins if enabled? " +
                                        " Or Main thread if blocking？\n\t Tips: Using this plugin, you should add @DiscoveryExposePoints on your Application's entrance."
                        );
                    }
                    preCountDelaySecond = delaySecond;
                    if(delaySecond >= 10){
                        enable = false;
                        log.info(noEnableInfos);
                        return RunningInvoking.getErrorInfo(noEnableInfos);
                    }
                }
            } catch (Throwable e) {
                return RunningInvoking.getErrorInfo("获取调度预环境异常 : " + e.getCause());
            }
        }
        if(executorService == null){
            initThreadPool();
        }
        return null;
    }

    @Data
    public static class InvokerBucket{
        //方法描述
        @NotNull
        String functionDescription;
        //调度对象类名
        @NotNull
        String className;
        //调度的方法名
        @NotNull
        String methodName;
        //形参表列以及参数值
        @Nullable
        ArrayList<InvokerParams> params;

        @Data
        public static class InvokerParams{
            String paramDoc;
            String paramClassName;
            Object paramValue;
        }
    }

    /**
     * Reflect invoke error.
     * @param message
     * @return
     */
    public static Object getErrorInfo(String message){
        Map<String, Object> errorInfo = RunningInvoking.errorInfoSupplier.get();
        errorInfo.put("invoke_res",message);
        return errorInfo;
    }

    /**
     * Reflect invoke biz success.
     * @param data
     * @return
     */
    public static Object getSuccessInfo(Object data){
        Map<String, Object> errorInfo = RunningInvoking.successInfoSupplier.get();
        errorInfo.put("invoke_res",data);
        return errorInfo;
    }

}